package simulator;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;
/**
 * This GameState class stores everything needed to exactly reproduce the current GameState
 * It does minimal actual simulation its self, since we contained simulation to the Simulator class
 */
public class GameState {
	// The current Game Time
	int time;
	
	WorkerState defaultWorkerAssignment;
	
	// Adjustment values, so we don't discard rounded parts of minerals
	double mineralAdjustment;
	double gasAdjustment;
	
	// Whole numbers of every GameAsset the GameState
	HashMap<GameAsset, Integer> assetNumbers;
	// The number of build slots the GameState has available in all GameAssets (realistically only buildings though)
	HashMap<GameAsset, Integer> assetFreeSlots;
	
	// What workers are working on.
	HashMap<WorkerState, Integer> workerAssignments;
	
	// GameAsset's currently being built are stored here
	ArrayList<BuildFinishEvent> pendingEvents;
	
	public GameState(HashMap<GameAsset, Integer> initialAssets) {
		assetNumbers = initialAssets;
		assetFreeSlots = new HashMap<GameAsset, Integer>();
		workerAssignments = new HashMap<WorkerState, Integer>();
		workerAssignments.put(WorkerState.GET_MINERALS,	assetNumbers.get(GameAsset.WORKER));
		workerAssignments.put(WorkerState.GET_VESPENE,	0);
		workerAssignments.put(WorkerState.BUILDING,	0);
		defaultWorkerAssignment = WorkerState.GET_MINERALS;
		
		// This ensures that for every GameAsset there is a value stored in the HashMap, even if it is zero.
		// This means that any call to get a GameAsset from the hashmap will not be met with a nullpointer.
		// We also want to get the appropriate Free Slots for each GameAsset.
		for(GameAsset asset : GameAsset.values()) {
			if(!assetNumbers.containsKey(asset)) {
				assetNumbers.put(asset, 0);
				assetFreeSlots.put(asset, 0);
			}
			else if(GameDataStore.getGameObjectFromGameAsset(asset) != null) {
				assetFreeSlots.put(asset, GameDataStore.getGameObjectFromGameAsset(asset).buildSlots);
			}
		}
		
		pendingEvents = new ArrayList<BuildFinishEvent>();
		time = 0;
	}
	
	/**
	 * Implementing our own copy constructor because java's cloneable interface seems counter-intuitive
	 * @param copy
	 */
	@SuppressWarnings("unchecked")
	public GameState(GameState copy) {
		this.time = copy.time;
		this.mineralAdjustment = copy.mineralAdjustment;
		this.gasAdjustment = copy.gasAdjustment;
		this.assetNumbers = (HashMap<GameAsset, Integer>)copy.assetNumbers.clone();
		this.assetFreeSlots = (HashMap<GameAsset, Integer>)copy.assetFreeSlots.clone();
		this.workerAssignments = (HashMap<WorkerState, Integer>)copy.workerAssignments.clone();
		this.defaultWorkerAssignment = copy.defaultWorkerAssignment;
		pendingEvents = (ArrayList<BuildFinishEvent>)copy.pendingEvents.clone();
	}
	
	/**
	 * Add a unit build event to the GameState
	 * This unit will be added to the assetNumbers upon completion
	 * @param event
	 */
	public void addBuildFinishEvent(BuildFinishEvent e) {
		pendingEvents.add(e);
	}
	
	/**
	 * Various ways to add Units to the GameState
	 * @param asset
	 */
	public void addUnit(GameAsset asset) {
		this.addUnit(asset, 1);
	}
	public void addUnit(GameAsset asset, int number) {
		if(asset == GameAsset.WORKER) {
			workerAssignments.put(defaultWorkerAssignment, workerAssignments.get(defaultWorkerAssignment) + 1);
		}
		assetNumbers.put(asset, assetNumbers.get(asset) + number);
	}
	public void setUnitNumber(GameAsset asset, int number) {
		assetNumbers.put(asset, number);
	}
	
	/**
	 * Get the time the GameState is at
	 * @return
	 */
	public int getTime() {
		return time;
	}
	
	/**
	 * Set the number of workers working on each task
	 * @param numMinerals
	 * @param numVespene
	 */
	public void setWorkersTask(int numMinerals, int numVespene) {
		// If the total number this function was called with doesn't equal the total number possible to work
		// throw error
		if((numMinerals + numVespene) != (this.getNumberOfAsset(GameAsset.WORKER) - this.getNumWorkersInState(WorkerState.BUILDING)) ) {
			throw new Error("setWorkersTask called with invalid numbers of workers");
		}
		this.workerAssignments.put(WorkerState.GET_MINERALS, numMinerals);
		this.workerAssignments.put(WorkerState.GET_VESPENE, numVespene);
	}
	/**
	 * Sets the default assignment of workers
	 * This is used to set the assignment of a worker when a new worker is produced
	 * @param state
	 */
	public void setWorkerDefaultTask(WorkerState state) {
		defaultWorkerAssignment = state;
	}
	
	/**
	 * Returned the number of workers in the specified WorkerState
	 * @param state
	 * @return number of workers in that state
	 */
	public int getNumWorkersInState(WorkerState state) {
		Integer value = this.workerAssignments.get(state);
		if(value == null) {
			return 0;
		}
		
		return value;
	}
	
	/**
	 * Gets the number of that asset the GameState currently has
	 * @param asset
	 * @return number of asset
	 */
	public int getNumberOfAsset(GameAsset asset) {
		return assetNumbers.get(asset);
	}
	
	/**
	 * Gets the number of that asset the GameState currently has, including those being built
	 * @param asset
	 * @return number of asset
	 */
	public int getTotalNumberOfAssets(GameAsset asset) {
		int totalNumber = assetNumbers.get(asset);
		
		for(BuildFinishEvent event : this.pendingEvents) {
			if(event.getGameAsset() == asset) {
				totalNumber++;
			}
		}
		
		return totalNumber;
	}
	
	/**
	 * Compares this instance of GameState with the target gamestate. If all values in this instance are greater than
	 * the target. Return true 
	 * @param target
	 * @return 
	 */
	public boolean reachedTarget(GameState target) {
		
		for(Entry<GameAsset, Integer> unitAmount : this.assetNumbers.entrySet()) {
			
			if(target.assetNumbers.get(unitAmount.getKey()) > unitAmount.getValue()) {
				return false;
			}
		}
		return true;
		
	}
	
	/**
	 * Checks if this GameState has passed a Maximum GameState passed in. This is subtly
	 * different to the reachedTarget method as this method must ignore certain assets
	 * @param maximum
	 * @return true if it's passed the maximum target
	 */
	public boolean passedMaximum(GameState maximum) {
		
		for(Entry<GameAsset, Integer> unitAmount : this.assetNumbers.entrySet()) {
			
			if(unitAmount.getKey() != GameAsset.SUPPLY) {
				
				if(maximum.assetNumbers.get(unitAmount.getKey()) < unitAmount.getValue()) {
					return true;
				}
			}
		}
		return false;
	}
	/**
	 * Checks to see if we have reached the minimum target including those being built
	 * @param minimumTarget
	 * @return
	 */
	public boolean reachedTargetTotal(GameState target) {
		for(GameAsset asset : GameAsset.values()) {
			
			if(target.getTotalNumberOfAssets(asset) > this.getTotalNumberOfAssets(asset)) {
				return false;
			}
		}
		return true;
	}
}

